package org.datacoder.benchmark

import javax.xml.parsers.*
import javax.xml.transform.*
import javax.xml.transform.dom.*
import javax.xml.transform.stream.*
import org.w3c.dom.*
import org.xml.sax.*

import org.datacoder.geocoding.datastructures.intervals.*
import org.datacoder.geocoding.datastructures.points.*
import org.datacoder.geocoding.datastructures.rectangles.*
import org.datacoder.util.*

/*
 * Loads benchmarking results from XML files, and writes them
 * out to XML, as well.  Has a number of methods for printing
 * the data in different formats, including in comma-delimited
 * format for Excel.
 */
class ResultHolder {

    private static final DATA_FOLDER = "data"

    Map<Class, FinderStatHolder> ihiResults
    Map<Class, FinderStatHolder> iviResults
    Map<Class, FinderStatHolder> pwrResults
    Map<Class, FinderStatHolder> rcpResults
    int numRuns

    public ResultHolder(){
        initialize()
    }
    public ResultHolder(String filename){
        if(!new File(constructXMLFilename(filename)).exists()){
            initialize()
        }else{
            ihiResults = new TreeMap<Class, FinderStatHolder>(new ClassNameComparator())
            iviResults = new TreeMap<Class, FinderStatHolder>(new ClassNameComparator())
            pwrResults = new TreeMap<Class, FinderStatHolder>(new ClassNameComparator())
            rcpResults = new TreeMap<Class, FinderStatHolder>(new ClassNameComparator())
            loadXMLFile(filename)
        }
    }
    private void initialize(){
        ihiResults = new TreeMap<Class, FinderStatHolder>(new ClassNameComparator())
        LinkedList<Class> ihiFinderList = new LinkedList<Class>()
        GeneralUtils.initializeClassList(GeneralUtils.INTERVALS_PACKAGE, IntersectingIntervalsFinder, ihiFinderList)
        for(finder in ihiFinderList){
            ihiResults.put(finder, new FinderStatHolder(finder))
        }
        iviResults = new TreeMap<Class, FinderStatHolder>(new ClassNameComparator())
        LinkedList<Class> iviFinderList = new LinkedList<Class>()
        GeneralUtils.initializeClassList(GeneralUtils.INTERVALS_PACKAGE, IntersectingIntervalsFinder, iviFinderList)
        for(finder in iviFinderList){
            iviResults.put(finder, new FinderStatHolder(finder))
        }
        pwrResults = new TreeMap<Class, FinderStatHolder>(new ClassNameComparator())
        LinkedList<Class> pwrFinderList = new LinkedList<Class>()
        GeneralUtils.initializeClassList(GeneralUtils.POINTS_PACKAGE, PointsWithinRectangleFinder, pwrFinderList)
        for(finder in pwrFinderList){
            pwrResults.put(finder, new FinderStatHolder(finder))
        }
        rcpResults = new TreeMap<Class, FinderStatHolder>(new ClassNameComparator())
        LinkedList<Class> rcpFinderList = new LinkedList<Class>()
        GeneralUtils.initializeClassList(GeneralUtils.RECTANGLES_PACKAGE, RectanglesContainingPointFinder, rcpFinderList)
        for(finder in rcpFinderList){
            rcpResults.put(finder, new FinderStatHolder(finder))
        }
    }

    public void printResults(int index, boolean verbose, boolean includeIntersectingVtIntervals){
        int maxLength = getMaxLength()
        println "IntersectingIntervalsFinders (HZ)"
        printFinderResults(ihiResults, index, verbose, maxLength)
        if(includeIntersectingVtIntervals){
            println "IntersectingIntervalsFinders (VT)"
            printFinderResults(iviResults, index, verbose, maxLength)
        }
        println "PointsWithinRectangleFinders"
        printFinderResults(pwrResults, index, verbose, maxLength)
        println "RectanglesContainingPointFinders"
        printFinderResults(rcpResults, index, verbose, maxLength)
    }
    private void printFinderResults(Map<Class, FinderStatHolder> results, int index, boolean verbose, int maxLength){
        for(finderClass in results.keySet()){
            FinderStatHolder fsHolder = results.get(finderClass)
            int localIndex = index
            if(localIndex > fsHolder.numRuns - 1){
                localIndex = fsHolder.numRuns - 1
            }
            if(localIndex < 0){
                continue
            }
            if(verbose){
                System.out.printf("\t${localIndex + 1}-%-${maxLength}s: Startup %05.5f seconds; Query %05.5f seconds\n", finderClass.getSimpleName(), fsHolder.startUpTimes.get(localIndex), fsHolder.averageQueryTimes.get(localIndex))
                for(jurisClass in fsHolder.byJurisTimes.keySet()){
                    LinkedList<Double> times = fsHolder.byJurisTimes.get(jurisClass)
                    System.out.printf("\t\t%-${maxLength - 16}s: %05.5f seconds\n", jurisClass.retrieveShortName(), times.get(localIndex))
                }
            }else{
                System.out.printf("\t${localIndex + 1}-%-${maxLength}s: %05.5f seconds\n", finderClass.getSimpleName(), fsHolder.averageQueryTimes.get(localIndex))
            }
        }
    }
    public void printStatResults(boolean verbose, boolean includeIntersectingVtIntervals){
        int maxLength = getMaxLength()
        println "IntersectingIntervalsFinders (HZ)"
        printFinderStatResults(ihiResults, verbose)
        if(includeIntersectingVtIntervals){
            println "IntersectingIntervalsFinders (VT)"
            printFinderStatResults(iviResults, verbose)
        }
        println "PointsWithinRectangleFinders"
        printFinderStatResults(pwrResults, verbose)
        println "RectanglesContainingPointFinders"
        printFinderStatResults(rcpResults, verbose)
    }
    private void printFinderStatResults(Map<Class, FinderStatHolder> results, boolean verbose){
        for(finderClass in results.keySet()){
            FinderStatHolder fsHolder = results.get(finderClass)
            fsHolder.calculateStatistics()
            System.out.println("\t${fsHolder.numRuns}-${finderClass.getSimpleName()}:")
            System.out.printf("\t\tStart Up: Mean %06.6f seconds, SD %06.6f seconds, Confidence %06.6f seconds---%06.6f seconds\n",
                fsHolder.startUpStats.mean, fsHolder.startUpStats.sd, fsHolder.startUpStats.lowerConf, fsHolder.startUpStats.upperConf)
            System.out.printf("\t\tQuery   : Mean %06.6f seconds, SD %06.6f seconds, Confidence %06.6f seconds---%06.6f seconds\n",
                fsHolder.averageQueryStats.mean, fsHolder.averageQueryStats.sd, fsHolder.averageQueryStats.lowerConf, fsHolder.averageQueryStats.upperConf)
            if(verbose){
                for(jurisClass in fsHolder.byJurisTimes.keySet()){
                    StatHolder jurisStats = fsHolder.byJurisStats.get(jurisClass)
                    System.out.printf("\t\t%-${8}s: Mean %06.6f seconds, SD %06.6f seconds, Confidence %06.6f seconds---%06.6f seconds\n",
                        jurisClass.retrieveShortName(), jurisStats.mean, jurisStats.sd, jurisStats.lowerConf, jurisStats.upperConf)
                }
            }
        }
    }
    private int getMaxLength(){
        int maxLength = 0
        for(clazz in (ihiResults.keySet())){
            int length = clazz.getSimpleName().length()
            if(length > maxLength){
                maxLength = length
            }
        }
        for(clazz in (pwrResults.keySet())){
            int length = clazz.getSimpleName().length()
            if(length > maxLength){
                maxLength = length
            }
        }
        for(clazz in (rcpResults.keySet())){
            int length = clazz.getSimpleName().length()
            if(length > maxLength){
                maxLength = length
            }
        }
        return maxLength
    }
    public writeXMLFile(String filename){
        Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
        Element docElement = doc.createElement("resultHolder")
        docElement.setAttribute("numRuns", Integer.toString(numRuns))
        docElement.appendChild(createResultsElement(doc, ihiResults, "ihiResults"))
        docElement.appendChild(createResultsElement(doc, iviResults, "iviResults"))
        docElement.appendChild(createResultsElement(doc, pwrResults, "pwrResults"))
        docElement.appendChild(createResultsElement(doc, rcpResults, "rcpResults"))
        doc.appendChild(docElement)

        // Write to file
        Source source = new DOMSource(doc)
        String fullFilename = constructXMLFilename(filename)
        File xmlFile = new File(fullFilename)
        if(xmlFile.exists()){
            println "Deleting old results file..."
            xmlFile.delete()
        }
        Result result = new StreamResult(xmlFile)
        Transformer transformer = TransformerFactory.newInstance().newTransformer()
        transformer.setOutputProperty(OutputKeys.STANDALONE, "yes")
        transformer.setOutputProperty(OutputKeys.INDENT, "yes")
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
        transformer.transform(source, result)
        println "Wrote results to ${fullFilename}."
    }
    private Element createResultsElement(Document doc, Map<Class, FinderStatHolder> results, String resultSetName){
        Element resultsElement = doc.createElement("results")
        resultsElement.setAttribute("resultSet", resultSetName)
        for(finderClass in results.keySet()){
            FinderStatHolder fsHolder = results.get(finderClass)
            Element finderClassElement = doc.createElement("finderClass")
            finderClassElement.setAttribute("class", fsHolder.clazz.getSimpleName())
            finderClassElement.setAttribute("numRuns", Integer.toString(fsHolder.numRuns))

            Element startUpTimesElement = doc.createElement("startUpTimes")
            for(startUpTime in fsHolder.startUpTimes){
                Element startUpTimeElement = doc.createElement("startUpTime")
                startUpTimeElement.setAttribute("time", Double.toString(startUpTime))
                startUpTimesElement.appendChild(startUpTimeElement)
            }
            finderClassElement.appendChild(startUpTimesElement)

            Element averageQueryTimesElement = doc.createElement("averageQueryTimes")
            for(averageQueryTime in fsHolder.averageQueryTimes){
                Element averageQueryTimeElement = doc.createElement("averageQueryTime")
                averageQueryTimeElement.setAttribute("time", Double.toString(averageQueryTime))
                averageQueryTimesElement.appendChild(averageQueryTimeElement)
            }
            finderClassElement.appendChild(averageQueryTimesElement)

            Element byJurisTimesElement = doc.createElement("byJurisTimes")
            for(jurisClass in fsHolder.byJurisTimes.keySet()){
                Element jurisElement = doc.createElement("juris")
                jurisElement.setAttribute("jurisClass", jurisClass.getSimpleName())
                LinkedList<Double> queryTimes = fsHolder.byJurisTimes.get(jurisClass)
                for(queryTime in queryTimes){
                    Element queryTimeElement = doc.createElement("queryTime")
                    queryTimeElement.setAttribute("time", Double.toString(queryTime))
                    jurisElement.appendChild(queryTimeElement)
                }
                byJurisTimesElement.appendChild(jurisElement)
            }
            finderClassElement.appendChild(byJurisTimesElement)

            resultsElement.appendChild(finderClassElement)
        }
        return resultsElement
    }

    private loadXMLFile(String filename){
        String fullFilename = constructXMLFilename(filename)
        Document doc
        try{
            doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(fullFilename)
        }catch(IOException ioe){
            System.err.println("IOException while loading ${fullFilename}: ${ioe.getMessage()}")
            System.exit(1)
        }catch(SAXException saxe){
            System.err.println("SAXException while loading ${fullFilename}: ${saxe.getMessage()}")
            System.exit(1)
        }
        Element docElement = doc.getDocumentElement()
        numRuns = Integer.parseInt(docElement.getAttribute("numRuns"))

        NodeList resultsElements = docElement.getChildNodes()
        List<Element> castResultsElements = new ArrayList<Element>()
        int resultsElementsLength = resultsElements.getLength()
        for(int i = 0; i < resultsElementsLength; i++){
            Node resultsNode = resultsElements.item(i)
            if(!(resultsNode instanceof Element)){
                continue
            }
            castResultsElements.add((Element)resultsNode)
        }
        loadResultsElement(ihiResults, castResultsElements.get(0))
        loadResultsElement(iviResults, castResultsElements.get(1))
        loadResultsElement(pwrResults, castResultsElements.get(2))
        loadResultsElement(rcpResults, castResultsElements.get(3))

    }
    private void loadResultsElement(Map<Class, FinderStatHolder> results, Element resultsElement){
        NodeList finderClassElements = resultsElement.getChildNodes()
        int finderClassElementsLength = finderClassElements.getLength()
        for(int j = 0; j < finderClassElementsLength; j++){
            Node finderClassNode = finderClassElements.item(j)
            if(!(finderClassNode instanceof Element)){
                continue
            }
            Element finderClassElement = (Element)finderClassNode
            String finderClassSimpleName = finderClassElement.getAttribute("class")
            Class finderClass
            try{
                try{
                    finderClass = Thread.currentThread().getContextClassLoader().loadClass("${GeneralUtils.INTERVALS_PACKAGE}.${finderClassSimpleName}")
                }catch(ClassNotFoundException cnfe1){
                    finderClass = Thread.currentThread().getContextClassLoader().loadClass("${GeneralUtils.POINTS_PACKAGE}.${finderClassSimpleName}")
                }
            }catch(ClassNotFoundException cnfe2){
                finderClass = Thread.currentThread().getContextClassLoader().loadClass("${GeneralUtils.RECTANGLES_PACKAGE}.${finderClassSimpleName}")
            }
            FinderStatHolder fsHolder = new FinderStatHolder(finderClass)
            fsHolder.numRuns = Integer.parseInt(finderClassElement.getAttribute("numRuns"))

            NodeList finderClassChildElements = finderClassElement.getChildNodes()
            List<Element> castFinderClassChildElements = new ArrayList<Element>()
            int finderClassChildElementsLength = finderClassChildElements.getLength()
            for(int i = 0; i < finderClassChildElementsLength; i++){
                Node finderClassChildNode = finderClassChildElements.item(i)
                if(!(finderClassChildNode instanceof Element)){
                    continue
                }
                castFinderClassChildElements.add((Element)finderClassChildNode)
            }

            Element startUpTimesElement = castFinderClassChildElements.get(0)
            NodeList startUpTimeElements = startUpTimesElement.getChildNodes()
            int startUpTimeElementsLength = startUpTimeElements.getLength()
            for(int k = 0; k < startUpTimeElementsLength; k++){
                Node startUpTimeNode = startUpTimeElements.item(k)
                if(!(startUpTimeNode instanceof Element)){
                    continue
                }
                fsHolder.startUpTimes.add(Double.parseDouble(((Element)startUpTimeNode).getAttribute("time")))
            }

            Element averageQueryTimesElement = castFinderClassChildElements.get(1)
            NodeList averageQueryTimeElements = averageQueryTimesElement.getChildNodes()
            int averageQueryTimeElementsLength = averageQueryTimeElements.getLength()
            for(int k = 0; k < averageQueryTimeElementsLength; k++){
                Node averageQueryTimeNode = averageQueryTimeElements.item(k)
                if(!(averageQueryTimeNode instanceof Element)){
                    continue
                }
                fsHolder.averageQueryTimes.add(Double.parseDouble(((Element)averageQueryTimeNode).getAttribute("time")))
            }

            Element byJurisTimesElement = castFinderClassChildElements.get(2)
            NodeList byJurisTimesElements = byJurisTimesElement.getChildNodes()
            int byJurisTimesElementsLength = byJurisTimesElements.getLength()
            for(int k = 0; k < byJurisTimesElementsLength; k++){

                Node jurisNode = byJurisTimesElements.item(k)
                if(!(jurisNode instanceof Element)){
                    continue
                }

                Element jurisElement = (Element)jurisNode
                String jurisClassSimpleName = jurisElement.getAttribute("jurisClass")
                Class jurisClass
                try{
                    jurisClass = Thread.currentThread().getContextClassLoader().loadClass("${GeneralUtils.JURISDICTION_PACKAGE}.${jurisClassSimpleName}")
                }catch(ClassNotFoundException cnfe){
                    System.err.println("Juris ClassNotFoundException in loadXMLFile: ${cnfe.getMessage()}")
                    System.exit(1)
                }

                LinkedList<Double> queryTimes = fsHolder.byJurisTimes.get(jurisClass)
                NodeList queryTimesElements = jurisElement.getChildNodes()
                int queryTimesElementsLength = queryTimesElements.getLength()
                for(int l = 0; l < queryTimesElementsLength; l++){
                    Node queryTimeNode = queryTimesElements.item(l)
                    if(!(queryTimeNode instanceof Element)){
                        continue
                    }
                    queryTimes.add(Double.parseDouble(((Element)queryTimeNode).getAttribute("time")))
                }

            }
            results.put(finderClass, fsHolder)
        }
    }
    private String constructXMLFilename(String filename){
        return "${DATA_FOLDER}/${filename}.xml"
    }



    private static printForExcel(String attr, String... filenames){
        ResultHolder[] rhs = new ResultHolder[filenames.length]
        Class jurisClass
        try{
            jurisClass = Thread.currentThread().getContextClassLoader().loadClass("${GeneralUtils.JURISDICTION_PACKAGE}.${attr}")
        }catch(Exception e){}
        for(int i = 0; i < filenames.length; i++){
            rhs[i] = new ResultHolder(filenames[i])
        }
        print ","
        for(filename in filenames){
            print "${filename},"
        }
        println ""
        // IHI Finders
        SortedSet<Class> ihiClasses = rhs[0].ihiResults.keySet()
        for(clazz in ihiClasses){
            print "${clazz.getSimpleName()},"
            for(rh in rhs){
                FinderStatHolder fsh = rh.ihiResults.get(clazz)
                fsh.calculateStatistics()
                if(attr.equals("startup")){
                    print "${fsh.startUpStats.mean},"
                }else if(attr.equals("query")){
                    print "${fsh.averageQueryStats.mean*10000},"
                }else{
                    print "${fsh.byJurisStats.get(jurisClass).mean*10000},"
                }
            }
            println ""
        }
        // PWR Finders
        SortedSet<Class> pwrClasses = rhs[0].pwrResults.keySet()
        for(clazz in pwrClasses){
            print "${clazz.getSimpleName()},"
            for(rh in rhs){
                FinderStatHolder fsh = rh.pwrResults.get(clazz)
                fsh.calculateStatistics()
                if(attr.equals("startup")){
                    print "${fsh.startUpStats.mean},"
                }else if(attr.equals("query")){
                    print "${fsh.averageQueryStats.mean*10000},"
                }else{
                    print "${fsh.byJurisStats.get(jurisClass).mean*10000},"
                }
            }
            println ""
        }
        // RCP Finders
        SortedSet<Class> rcpClasses = rhs[0].rcpResults.keySet()
        for(clazz in rcpClasses){
            print "${clazz.getSimpleName()},"
            for(rh in rhs){
                FinderStatHolder fsh = rh.rcpResults.get(clazz)
                fsh.calculateStatistics()
                if(attr.equals("startup")){
                    print "${fsh.startUpStats.mean},"
                }else if(attr.equals("query")){
                    print "${fsh.averageQueryStats.mean*10000},"
                }else{
                    print "${fsh.byJurisStats.get(jurisClass).mean*10000},"
                }
            }
            println ""
        }
    }

}

